package com.zenika.zentracker.hibernate.jdbc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.hibernate.Interceptor;

/**
 * Class acting as a Singleton. Every Hibernate <tt>Session</tt> has a an Interceptor. In the worst case, an
 * EmptyInterceptor.<br/>
 * 
 * This class mainly holds a Map where the key is the Interceptor, and the value is a Map of all the executed queries
 * and the executions time of those queries.<br/>
 * 
 * The map used is a <tt>WeakHashMap</tt> so when a Session is destroyed, they will be no more reference on the
 * Session <tt>Interceptor</tt> and so it will be garbage collected from the Map.</br>
 * 
 * @author Zenika
 * 
 */
public class SQLSniffer {

    private static SQLSniffer sniffer;
    private Map interceptorsMap = new WeakHashMap();

    private SQLSniffer() {

    }

    /**
     * @return
     */
    public static synchronized SQLSniffer getInstance() {
        if (sniffer == null) {
            sniffer = new SQLSniffer();
        }

        return sniffer;
    }

    /**
     * @param executionTime
     */
    public void addSQLExecution(Interceptor interceptor, String query, long executionTime) {
        Map sqlExecutionsMap = (Map) this.interceptorsMap.get(interceptor);
        if (sqlExecutionsMap == null) {
            sqlExecutionsMap = new HashMap();
            interceptorsMap.put(interceptor, sqlExecutionsMap);
        }

        addSQLExecution(sqlExecutionsMap, query, executionTime);
    }

    /**
     * Called after an SQLExecution. It should track the execution time of a SQL query. The SQL query could come from
     * HQL Queries, SQL Queries, Criteria APIs and the Session API.
     * 
     * Once the SQL and the execution time are determined, they will be stored into the Interceptor via this method
     * before tracking.
     * 
     * @param sqlExecutions Map of all the SQL executions linked to an <tt>Interceptor</tt>
     * @query The SQL string without the Hibernate comments, even if comments are set in the configuration file.
     * @executionTime Execution time of the sql. From the execution of the Statement to the return of the ResultSet.
     */
    private void addSQLExecution(Map sqlExecutions, String query, long executionTime) {
        String cleanQuery = removeCommentsInformation(query);
        List list = (List) sqlExecutions.get(cleanQuery);
        if (list == null) {
            List newList = new ArrayList();
            newList.add(new Long(executionTime));
            sqlExecutions.put(cleanQuery, newList);
        } else {
            list.add(new Long(executionTime));
        }
    }

    /**
     * Remove the Hibernate comment from the query String if the comment has been activated via the property.
     * 
     * @param query The SQL query to log
     * @return Query without the comments
     */
    private String removeCommentsInformation(String query) {
        // Will use a constant when the Hibernate Framework will use one ;)
        // Check class org.hibernate.sql.InsertSelect, org.hibernate.sql.Delete
        int endCommentIndex = query.indexOf("*/");
        if (endCommentIndex > -1) {
            return (query.substring(endCommentIndex + 2)).trim();
        }
        return query;
    }

    /**
     * Log the SQL execution times per SQL query.
     * 
     * @param entities Key, a SQL query. Value, a list of execution times.
     */
    public List logAndDumpSqlExecutionInformation(Interceptor interceptor) {
        Map sqlExecutionsMap = (Map) this.interceptorsMap.get(interceptor);
        List executionTimesResult = new ArrayList();
        
        if (sqlExecutionsMap == null)
            return executionTimesResult;
        
        Set keySet = sqlExecutionsMap.keySet();

        for (Iterator iterator = keySet.iterator(); iterator.hasNext();) {
            String query = (String) iterator.next();

            List executionTimes = (List) sqlExecutionsMap.get(query);
            long sum = 0;

            for (int i = 0; i < executionTimes.size(); i++) {
                sum += ((Long) executionTimes.get(i)).longValue();
            }

            int times = executionTimes.size();
            StringBuffer buffer = new StringBuffer();
            buffer.append("Query <").append(query).append("> ").append("executed: ");
            buffer.append(times).append(" times. ").append("average: ").append(sum / times);
            buffer.append(" min: ").append(Collections.min(executionTimes)).append(" max: ");
            buffer.append(Collections.max(executionTimes));
            
            executionTimesResult.add(buffer.toString());
        }

        interceptorsMap.remove(interceptor);

        return executionTimesResult;
    }
}
